import pygame.sprite
#from common import *
from gofactory import GameObjectFactory
from net import *

from enemies import Enemy
from grid_collide import groupcollide_grid,Grid

class GameEngine(object):
    ID = 0
    'Main Game engine'
    def __init__(self,game_map,net_controller,soundplayer):
        self.soundplayer = soundplayer
        self.setNetController(net_controller)

        self.game_map = game_map
        self.relative_speed = game_map.relative_speed
        self.friction = 65*self.relative_speed
        self.gravitation = 80*self.relative_speed
        
        self.bases = game_map.bases
        self.id = net_controller.id
        self.go_factory = GameObjectFactory(self)
        # set up groups
        self.ships = pygame.sprite.Group()
        self.shots = pygame.sprite.Group()
        self.enemies = pygame.sprite.Group()

        self.vessels = pygame.sprite.Group()
        
        self.all_sprites = RenderToViewPort()

        self.players = GameObjectGroup()
        self.remote_players = GameObjectGroup()
        self.local_players = GameObjectGroup()
        self.local_game_objects = GameObjectGroup()
        self.syncing_shots = GameObjectGroup()
        # all game objects
        self.game_objects = GameObjectGroup()    
        self.changed_go = GameObjectGroup() #holds gameobjects modyfied by server(me), dont forget to tell clients

        # game engine running
        self.running = 0
        
    def syncClients(self):
        for go in self.changed_go.sprites():
            self.net_controller.tellAllPeers(NetMessSyncGameObject(self.id ,None,go.id, go.getSync()) )    

    def getShips(self):
        return self.ships.sprites()

    def getPlayerPos(self,player):
        #return (self.players[player_no].sprite.rect[0],self.players[player_no].sprite.rect[1])
        return player.sprite.rect.center
        
    def setNetController(self,net_controller):
        self.net_controller = net_controller
        self.id = self.net_controller.id
        self.net_controller.setGameEngine(self)
        
    def addPlayer(self,player,local=1):
        ' adds a local or remote player '
        player.setGameEngine(self)
        # Set Starting pos
        try:
            startpos=self.game_map.startingPositions[len(self.players.sprites())]
        except IndexError:
            sys.stderr.write("Error Maximum players Reached\n")
            self.removePlayer(player,local=1)
            return
        # make startpos height be bottom of ship
#        startpos = (startpos[0],startpos[1]-player.ship.rect.height)
        player.setStartingPos(startpos)
        player.reset()
        print "adding go: %d"%player.id
        self.players.add((player,))
        self.game_objects.add((player,))
        self.addShip(player.sprite,local)
        if local:
            self.local_players.add((player,))
            # add player to all clients
            self.net_controller.tellAllPeers(NetMessAddPlayer(self.id,None,player))
        else:
            self.remote_players.add((player,))

    def removePlayer(self,player,local=1):
        ' removes a player '
        player.kill()
        if local:
            # add player to all clients
            self.net_controller.tellAllPeers(NetMessRemovePlayer(self.id,None,player))

    def removeGo(self,go,local=1):
        ' removes a shot (do this only for syncable shots) '
        go.kill()
        if local:
            # add player to all clients
            self.net_controller.tellAllPeers(NetMessRemoveGo(self.id,None,go))

    def respawnPlayer(self,player,dt=0,local=1):
        ''' resurect player after some time'''
        if local:
            player.respawn_time.tick(dt)
            if player.respawn_time.isSet():
                player.reset() # reset player values
                player.respawn_time.reset()
                self.addShip(player.sprite)
                self.net_controller.tellAllPeers(NetMessRespawnPlayer(self.id, None, player.id))
        else:
            player.reset() # reset player values
            self.addShip(player.sprite,local=0)

    def addShot(self,shot,local=1,nosound=0):
        self.game_objects.add((shot,))
        self.all_sprites.add((shot,))
        if shot.syncable:
            self.syncing_shots.add((shot,))
        else:
            self.shots.add((shot,))

        if local:
            self.net_controller.tellAllPeers(NetMessAddShot(self.id, None,
                                                              shot.__class__.__name__,
                                                              (shot.pos_x,shot.pos_y,
                                                               shot.vel_x,shot.vel_y),
                                                              {'owner_id':shot.owner_id,'id':shot.id}))
        if not nosound:
            self.soundplayer.play(shot.create_sound,0,1)
        
    def addShip(self,ship,local=1):
        self.game_objects.add((ship,))
        if local:
            self.local_game_objects.add((ship,))
##        else:
##            self.remote_game_objects.add((ship,))
            
        self.ships.add([ship,])
        self.vessels.add([ship,])
        self.all_sprites.add([ship,])
        
    def addEnemy(self,enemy,local=1):
        self.game_objects.add((enemy,))
        if local:
            self.local_game_objects.add((enemy,))
##        else:
##            self.remote_game_objects.add((ship,))
            
        self.enemies.add([enemy,])
        self.vessels.add([enemy,])
        self.all_sprites.add([enemy,])

    def shipKilled(self,ship,local=1):
        ' kill that ship '
        print "kill ship id:%d"%ship.id
        if local:
            # handle score
            if ship.last_hit_by != None:
                (ship.last_hit_by).score+=1
            else:
                ship.owner.score-=1
            self.net_controller.tellAllPeers(NetMessShipKilled(self.id, None, ship.id))
        ship.kill()

    def enemyKilled(self,enemy,local=1):
        ' kill that enemy '
        print "kill enemy id:%d"%enemy.id
        if local:
            # handle score
            if enemy.last_hit_by != None:
                (enemy.last_hit_by).score+=1
            # TODO: Use enemy killed net message
            self.net_controller.tellAllPeers(NetMessShipKilled(self.id, None, enemy.id))
        enemy.kill()
                
    def _manageHit(self,victim,shot):
        ' handles collision between victim&shot '
        victim.changeLife(-shot.power)
        # move victim on impact, dont move it down if it is landed
        if shot.vel_y<0 or not victim.landed:
            victim.setVel( (shot.vel_x*shot.mass+victim.mass*victim.vel_x)/victim.mass,
                         (shot.vel_y*shot.mass+victim.mass*victim.vel_y)/victim.mass )
        else:
            victim.setVel((shot.vel_x*shot.mass+victim.mass*victim.vel_x)/victim.mass,0 )
        # give cred!
        if isinstance(victim, Ship):
            if shot.owner_id != None and shot.owner_id != victim.owner.id:
                victim.last_hit_by = self.players.getById(shot.owner_id)
        elif shot.owner_id is not None and isinstance(victim, Enemy):
            victim.last_hit_by = self.players.getById(shot.owner_id)

    def _manageGoMapCollide(self,go):
        '''
        Manage game object crash with game map.
        '''
        r = pygame.Rect(go.rect)
        last_collide_rect = r
        last_x,last_y = go.pos_x,go.pos_y
        x,y = go.pos_x, go.pos_y
        
        # go between old pos and new pos and find first rect location that collies
        for x,y in self._points_between((go.old_pos_x,go.old_pos_y),(go.pos_x,go.pos_y)):
            r.center = (x,y)
            if self.game_map.rectCollide(r):
                last_collide_rect = r
                break
            else:
                # set game objects location to last pos that was not collided 
                go.rect.center = last_x,last_y
                go.pos_x, go.pos_y =  x,y
        else:
            r = last_collide_rect 
            last_collide_rect = go.rect
        


        # calculate the center point of all crash points
        collide_points = self.game_map.rectCollide(last_collide_rect,all=True)
        if  collide_points:
            coolide_center = [0,0]
            for cp in collide_points:
                coolide_center[0] += cp[0] 
                coolide_center[1] += cp[1] 
            coolide_center[0] = coolide_center[0]/len(collide_points)
            coolide_center[1] = coolide_center[1]/len(collide_points)
        else:
            #why would this happen, need to find out...
            coolide_center = [last_collide_rect.width/2,last_collide_rect.height/2]

        # set what directions collided
        go.collide_dirs = 0
        if coolide_center != r.center:
            if coolide_center[0] > r.center[0]:
                #left
                go.collide_dirs |= go.COLLIDE_LEFT
            elif coolide_center[0] < r.center[0]:
                #right
                go.collide_dirs |= go.COLLIDE_RIGHT
            if coolide_center[1] < r.center[1]:
                #top
                go.collide_dirs |= go.COLLIDE_TOP
            elif coolide_center[1] > r.center[1]:
                #bottom
                go.collide_dirs |= go.COLLIDE_BOTTOM

        # Bounce and damage
        damage = 0

        new_x_vel,new_y_vel = go.vel_x,go.vel_y 
        if (go.collide_dirs & (go.COLLIDE_LEFT|go.COLLIDE_RIGHT)):
            new_x_vel = -go.vel_x * go.bounce_ratio
            damage += abs(go.vel_x*go.vel_x*go.collide_damage_ratio)
                    
        if (go.collide_dirs & (go.COLLIDE_TOP|go.COLLIDE_BOTTOM)):
            new_y_vel = -go.vel_y * go.bounce_ratio
            damage += abs(go.vel_y*go.vel_y*go.collide_damage_ratio)

        #go.setVel(0,0)
        go.setVel(new_x_vel,new_y_vel)
        #print "damage",damage
        go.life.val -= damage

    def _points_between(self,start,end):
        '''
        Take a starting point and end point and get yield all the point in between
        '''
        sx,sy = start
        ex,ey = end
        odx,ody = sx-ex,sy-ey
        hyp = hypot(odx,ody)
        h = 1.0
        while h<=hyp:
            d = h/hyp
            dx = odx*d
            dy = ody*d
            h +=1.0
            yield sx-dx,sy-dy
        
    def begin(self):
        ''' setup game variabels '''
        self.running = 1
        # sync timing and flag
        self.sync_tick = Tick(0.08)

    def update(self,dt,keystate):
        ''' Main Game logic and timing '''

        # set sync tick flag at every sync_interval
        self.sync_tick.tick(dt)

        if self.soundplayer: self.soundplayer.update(dt)
            
        # apply gravitation and friction to ships
        for ship in self.ships.sprites():
            if not ship.landed:
                ship.decreaseVel(self.friction,dt)
                ship.changeVel(0,self.gravitation,dt)


        #handle player input
        for player in self.local_players.sprites():
            player.updateKeyPress(keystate,dt)
            player.update(dt)
            if player.ship.landed:
                # enter menu mode if player is docked
                player.controls._enableMenu()
            
            # tell server if we got player input
            if player.changed and self.sync_tick.isSet():
                self.sync_tick.reset() # reset sync tick
                player.changed = 0
                self.changed_go.add((player.ship,player)) # add to list of objects to tell clients about
                self.syncClients()
##                # sync changed player with server
##                self.net_controller.tellAllPeers( NetMessSyncGameObject(self.id, None, player.ship.id,player.ship.getSync()))        
##                self.net_controller.tellAllPeers( NetMessSyncGameObject(self.id, None, player.id,player.getSync()))        
            if not player.alive():
                print "%s Died!"% player.name
                self.exit()

    def _update(self,dt):
        # update all sprites
        self.all_sprites.update(dt)

        # Handle shot-gamemap collisions and max alive time
        pCollide = self.game_map.pointCollide
        for shot in self.shots.sprites():
            if pCollide(shot.rect.center):
                shot.vel_x, shot.vel_y = 0,0 # stop vel before explode
                shot.explode()
                shot.kill()
            elif shot.alive_time > shot.max_alive_time:
                shot.explode()
                shot.kill()

        for p in self.players.sprites():
            ship = p.ship
            if not ship.alive(): continue
            ship.collide = self.game_map.rectCollide(ship.rect)
            ship.checkLanded()
            ship.collide_dirs = 0
            if ship.collide:
                self._manageGoMapCollide(ship)
            #self.cur_tile_pos = self.game_map.getMapXYAtPos(ship.rect.center[0],ship.rect.bottom)

        #----------------------------------------
        # handle syncing shots
        collisions = pygame.sprite.groupcollide(self.vessels, self.syncing_shots, 0,0)
        for ship in collisions.keys():
            for shot in collisions[ship]:
                shot.explode()
                self.removeGo(shot)
                shot.kill()
                self._manageHit(ship,shot)


        for shot in self.syncing_shots.sprites():
            if pCollide(shot.rect.center):
                shot.vel_x, shot.vel_y = 0,0 # stop vel before explode
                shot.explode()
                self.removeGo(shot)
                shot.kill()
            elif shot.alive_time > shot.max_alive_time or shot.exploded:
                shot.explode()
                self.removeGo(shot)
                shot.kill()

        #----------------------------------------
        # Handle ship-shot collisions
#        collisions = pygame.sprite.groupcollide(self.vessels, self.shots, 0,0)
        shots_grid = Grid(self.shots.sprites(),self.game_map.rect[2:])
        try:
            collisions = groupcollide_grid(self.vessels, shots_grid, 0,1)
        except IndexError,e:
            print "grid pos out side of map: %s"%(e,)


        for ship in collisions.keys():
            self.changed_go.add((ship,)) # add ship to list of objects to tell clients about
            for shot in collisions[ship]:
                shot.explode()
                self._manageHit(ship,shot)

        # Handle ship-enemy collisions
        for player in self.players.sprites():
            if not player.ship.alive(): continue
            collide_enemies = pygame.sprite.spritecollide(player.sprite, self.enemies, 0)
            for enemy in collide_enemies:
                self.changed_go.add((enemy,)) # add ship to list of objects to tell clients about
                self.changed_go.add((player,)) # add ship to list of objects to tell clients about
                player.ship.changeLife(-100)
##                    vx1,vy1,m1 = float(s1.vel_x),float(s1.vel_y),float(s1.mass)
##                    vx2,vy2,m2 = float(s2.vel_x),float(s2.vel_y),float(s2.mass)
##                    s1.setVel( (vx2*m2+m1*vx1)/m1,(vy2*m2+m1*vy1)/m1 )
##                    s2.setVel( (vx2*m2+m1*vx1)/m2,(vy2*m2+m1*vy1)/m2 )
##                    s2.animate(dt)


        # Handle ship-ship collisions
        for player in self.players.sprites():
            if not player.ship.alive(): continue
            collide_ships = pygame.sprite.spritecollide(player.sprite, self.ships, 0)
            for ship in collide_ships:
                if ship != player.sprite:
                    self.changed_go.add((ship,)) # add ship to list of objects to tell clients about
                    self.changed_go.add((player,)) # add ship to list of objects to tell clients about
                    s1 = ship
                    s2 = player.ship
##                    vx1,vy1,m1 = float(s1.vel_x),float(s1.vel_y),float(s1.mass)
##                    vx2,vy2,m2 = float(s2.vel_x),float(s2.vel_y),float(s2.mass)
##                    s1.setVel( (vx2*m2+m1*vx1)/m1,(vy2*m2+m1*vy1)/m1 )
##                    s2.setVel( (vx2*m2+m1*vx1)/m2,(vy2*m2+m1*vy1)/m2 )
##                    s2.animate(dt)

                    s1.changeLife(-100)        
                    s2.changeLife(-100)

        for player in self.players.sprites():
            if player.ship.changed:
#            if player.changed:
#                self.changed_go.add((player,)) # add ship to list of objects to tell clients about
                self.changed_go.add((player.ship,))
#                player.changed = 0                
                player.ship.changed = 0                

    def exit(self):
        self.running = 0
        

class ServerGameEngine(GameEngine):
    'Game engine that handles network clients as well'
        
    def begin(self):
        ''' setup game variabels '''
        GameEngine.begin(self)
        self.all_sync_tick = Tick(1.0) # all players sync tick 


    def update(self,dt,keystate):
        ''' Server Main Game logic and timing '''
        GameEngine.update(self,dt,keystate)
        self.all_sync_tick.tick(dt)
        self._update(dt)
        # sync all clients at every tick
        if self.sync_tick.isSet():
            self.sync_tick.reset()
            self.syncClients()
            self.changed_go.empty()

        # sync all clients at every tick
        if self.all_sync_tick.isSet():
            print "syncing all"
            self.all_sync_tick.reset()
            self.syncAll()

    def syncAll(self):
        self.changed_go.add(self.syncing_shots.sprites())
        self.changed_go.add(self.ships.sprites())
        self.changed_go.add(self.players.sprites())
        self.syncClients()
        self.changed_go.empty()
        
    def exit(self):
        GameEngine.exit(self)
        
#---------------------------------------------------------------
class ClientGameEngine(GameEngine):
    'Main Client Game engine'

    def begin(self):
        ''' setup game variabels '''
        GameEngine.begin(self)
        self.sync_tick.tick_interval = 0.1

    def update(self,dt,keystate):
        ''' Client Main logic and timing '''
        GameEngine.update(self,dt,keystate)
        # update all sprites
        self.all_sprites.update(dt)

        # Handle shot-gamemap collisions and max alive time
        pCollide = self.game_map.pointCollide
        for shot in self.shots.sprites():
            if pCollide(shot.rect.center) or shot.alive_time > shot.max_alive_time:
                shot.kill()

        # Handle ship-shot collisions client does not change life, only delete shots
        collisions = pygame.sprite.groupcollide(self.ships, self.shots, 0,1)

#----------------------------------------------------------
#    Main function
#----------------------------------------------------------

class NoNetGameEngine(GameEngine):
    'Game engine that is not network enabled'
    def begin(self):
        ''' setup game variabels '''
        GameEngine.begin(self)
        # set sync tics long so that we dont waste effort on netowork oprations we dont use
        self.sync_tick = Tick(30)
        self.ping_tick = Tick(60)


    def update(self,dt,keystate):
        ''' Server Main Game logic and timing '''
        super(NoNetGameEngine,self).update(dt,keystate)
        # update all sprites
        self._update(dt)

